package com.cloud.rou.im.server;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import jdk.nashorn.internal.ir.annotations.Immutable;

import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;


public final class WaitStrategies {
    private static final WaitStrategy NO_WAIT_STRATEGY = new WaitStrategies.FixedWaitStrategy(0L);

    private WaitStrategies() {
    }

    public static WaitStrategy noWait() {
        return NO_WAIT_STRATEGY;
    }

    public static WaitStrategy fixedWait(long sleepTime,  TimeUnit timeUnit) throws IllegalStateException {
        Preconditions.checkNotNull(timeUnit, "The time unit may not be null");
        return new WaitStrategies.FixedWaitStrategy(timeUnit.toMillis(sleepTime));
    }

    public static WaitStrategy randomWait(long maximumTime,  TimeUnit timeUnit) {
        Preconditions.checkNotNull(timeUnit, "The time unit may not be null");
        return new WaitStrategies.RandomWaitStrategy(0L, timeUnit.toMillis(maximumTime));
    }

    public static WaitStrategy randomWait(long minimumTime,  TimeUnit minimumTimeUnit, long maximumTime,  TimeUnit maximumTimeUnit) {
        Preconditions.checkNotNull(minimumTimeUnit, "The minimum time unit may not be null");
        Preconditions.checkNotNull(maximumTimeUnit, "The maximum time unit may not be null");
        return new WaitStrategies.RandomWaitStrategy(minimumTimeUnit.toMillis(minimumTime), maximumTimeUnit.toMillis(maximumTime));
    }

    public static WaitStrategy incrementingWait(long initialSleepTime,  TimeUnit initialSleepTimeUnit, long increment,  TimeUnit incrementTimeUnit) {
        Preconditions.checkNotNull(initialSleepTimeUnit, "The initial sleep time unit may not be null");
        Preconditions.checkNotNull(incrementTimeUnit, "The increment time unit may not be null");
        return new WaitStrategies.IncrementingWaitStrategy(initialSleepTimeUnit.toMillis(initialSleepTime), incrementTimeUnit.toMillis(increment));
    }

    public static WaitStrategy exponentialWait() {
        return new WaitStrategies.ExponentialWaitStrategy(1L, 9223372036854775807L);
    }

    public static WaitStrategy exponentialWait(long maximumTime,  TimeUnit maximumTimeUnit) {
        Preconditions.checkNotNull(maximumTimeUnit, "The maximum time unit may not be null");
        return new WaitStrategies.ExponentialWaitStrategy(1L, maximumTimeUnit.toMillis(maximumTime));
    }

    public static WaitStrategy exponentialWait(long multiplier, long maximumTime,  TimeUnit maximumTimeUnit) {
        Preconditions.checkNotNull(maximumTimeUnit, "The maximum time unit may not be null");
        return new WaitStrategies.ExponentialWaitStrategy(multiplier, maximumTimeUnit.toMillis(maximumTime));
    }

    public static WaitStrategy fibonacciWait() {
        return new WaitStrategies.FibonacciWaitStrategy(1L, 9223372036854775807L);
    }

    public static WaitStrategy fibonacciWait(long maximumTime,  TimeUnit maximumTimeUnit) {
        Preconditions.checkNotNull(maximumTimeUnit, "The maximum time unit may not be null");
        return new WaitStrategies.FibonacciWaitStrategy(1L, maximumTimeUnit.toMillis(maximumTime));
    }

    public static WaitStrategy fibonacciWait(long multiplier, long maximumTime,  TimeUnit maximumTimeUnit) {
        Preconditions.checkNotNull(maximumTimeUnit, "The maximum time unit may not be null");
        return new WaitStrategies.FibonacciWaitStrategy(multiplier, maximumTimeUnit.toMillis(maximumTime));
    }

    public static <T extends Throwable> WaitStrategy exceptionWait( Class<T> exceptionClass,  Function<T, Long> function) {
        Preconditions.checkNotNull(exceptionClass, "exceptionClass may not be null");
        Preconditions.checkNotNull(function, "function may not be null");
        return new WaitStrategies.ExceptionWaitStrategy(exceptionClass, function);
    }

    public static WaitStrategy join(WaitStrategy... waitStrategies) {
        Preconditions.checkState(waitStrategies.length > 0, "Must have at least one wait strategy");
        List<WaitStrategy> waitStrategyList = Lists.newArrayList(waitStrategies);
        Preconditions.checkState(!waitStrategyList.contains((Object)null), "Cannot have a null wait strategy");
        return new WaitStrategies.CompositeWaitStrategy(waitStrategyList);
    }

    @Immutable
    private static final class ExceptionWaitStrategy<T extends Throwable> implements WaitStrategy {
        private final Class<T> exceptionClass;
        private final Function<T, Long> function;

        public ExceptionWaitStrategy( Class<T> exceptionClass,  Function<T, Long> function) {
            this.exceptionClass = exceptionClass;
            this.function = function;
        }

        public long computeSleepTime(Attempt lastAttempt) {
            if (lastAttempt.hasException()) {
                Throwable cause = lastAttempt.getExceptionCause();
                if (this.exceptionClass.isAssignableFrom(cause.getClass())) {
                    return (Long)this.function.apply((T) cause);
                }
            }

            return 0L;
        }
    }

    @Immutable
    private static final class CompositeWaitStrategy implements WaitStrategy {
        private final List<WaitStrategy> waitStrategies;

        public CompositeWaitStrategy(List<WaitStrategy> waitStrategies) {
            Preconditions.checkState(!waitStrategies.isEmpty(), "Need at least one wait strategy");
            this.waitStrategies = waitStrategies;
        }

        public long computeSleepTime(Attempt failedAttempt) {
            long waitTime = 0L;

            WaitStrategy waitStrategy;
            for(Iterator i$ = this.waitStrategies.iterator(); i$.hasNext(); waitTime += waitStrategy.computeSleepTime(failedAttempt)) {
                waitStrategy = (WaitStrategy)i$.next();
            }

            return waitTime;
        }
    }

    @Immutable
    private static final class FibonacciWaitStrategy implements WaitStrategy {
        private final long multiplier;
        private final long maximumWait;

        public FibonacciWaitStrategy(long multiplier, long maximumWait) {
            Preconditions.checkArgument(multiplier > 0L, "multiplier must be > 0 but is %d", new Object[]{multiplier});
            Preconditions.checkArgument(maximumWait >= 0L, "maximumWait must be >= 0 but is %d", new Object[]{maximumWait});
            Preconditions.checkArgument(multiplier < maximumWait, "multiplier must be < maximumWait but is %d", new Object[]{multiplier});
            this.multiplier = multiplier;
            this.maximumWait = maximumWait;
        }

        public long computeSleepTime(Attempt failedAttempt) {
            long fib = this.fib(failedAttempt.getAttemptNumber());
            long result = this.multiplier * fib;
            if (result > this.maximumWait || result < 0L) {
                result = this.maximumWait;
            }

            return result >= 0L ? result : 0L;
        }

        private long fib(long n) {
            if (n == 0L) {
                return 0L;
            } else if (n == 1L) {
                return 1L;
            } else {
                long prevPrev = 0L;
                long prev = 1L;
                long result = 0L;

                for(long i = 2L; i <= n; ++i) {
                    result = prev + prevPrev;
                    prevPrev = prev;
                    prev = result;
                }

                return result;
            }
        }
    }

    @Immutable
    private static final class ExponentialWaitStrategy implements WaitStrategy {
        private final long multiplier;
        private final long maximumWait;

        public ExponentialWaitStrategy(long multiplier, long maximumWait) {
            Preconditions.checkArgument(multiplier > 0L, "multiplier must be > 0 but is %d", new Object[]{multiplier});
            Preconditions.checkArgument(maximumWait >= 0L, "maximumWait must be >= 0 but is %d", new Object[]{maximumWait});
            Preconditions.checkArgument(multiplier < maximumWait, "multiplier must be < maximumWait but is %d", new Object[]{multiplier});
            this.multiplier = multiplier;
            this.maximumWait = maximumWait;
        }

        public long computeSleepTime(Attempt failedAttempt) {
            double exp = Math.pow(2.0D, (double)failedAttempt.getAttemptNumber());
            long result = Math.round((double)this.multiplier * exp);
            if (result > this.maximumWait) {
                result = this.maximumWait;
            }

            return result >= 0L ? result : 0L;
        }
    }

    @Immutable
    private static final class IncrementingWaitStrategy implements WaitStrategy {
        private final long initialSleepTime;
        private final long increment;

        public IncrementingWaitStrategy(long initialSleepTime, long increment) {
            Preconditions.checkArgument(initialSleepTime >= 0L, "initialSleepTime must be >= 0 but is %d", new Object[]{initialSleepTime});
            this.initialSleepTime = initialSleepTime;
            this.increment = increment;
        }

        public long computeSleepTime(Attempt failedAttempt) {
            long result = this.initialSleepTime + this.increment * (failedAttempt.getAttemptNumber() - 1L);
            return result >= 0L ? result : 0L;
        }
    }

    @Immutable
    private static final class RandomWaitStrategy implements WaitStrategy {
        private static final Random RANDOM = new Random();
        private final long minimum;
        private final long maximum;

        public RandomWaitStrategy(long minimum, long maximum) {
            Preconditions.checkArgument(minimum >= 0L, "minimum must be >= 0 but is %d", new Object[]{minimum});
            Preconditions.checkArgument(maximum > minimum, "maximum must be > minimum but maximum is %d and minimum is", new Object[]{maximum, minimum});
            this.minimum = minimum;
            this.maximum = maximum;
        }

        public long computeSleepTime(Attempt failedAttempt) {
            long t = Math.abs(RANDOM.nextLong()) % (this.maximum - this.minimum);
            return t + this.minimum;
        }
    }

    @Immutable
    private static final class FixedWaitStrategy implements WaitStrategy {
        private final long sleepTime;

        public FixedWaitStrategy(long sleepTime) {
            Preconditions.checkArgument(sleepTime >= 0L, "sleepTime must be >= 0 but is %d", new Object[]{sleepTime});
            this.sleepTime = sleepTime;
        }

        public long computeSleepTime(Attempt failedAttempt) {
            return this.sleepTime;
        }
    }
}
